package club.jdiy.utils.poi;

import club.jdiy.utils.DateUtils;
import org.springframework.http.MediaType;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.*;
import java.nio.charset.StandardCharsets;
import java.text.SimpleDateFormat;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.util.*;

/**
 * JDiy实现的一个Csv工具类，可以很方便的将POJO对象导出到Csv.
 * 参考示例代码：
 * <p/>
 * <pre>
 *     CsvUtils et = new CsvUtils();
 *     et.setHeader(new MetaSet[]{
 *         new MetaSet("姓名", "name"),   //列：表头和内容的导出映射关系 详见{@link MetaSet}类的各构造函数的用法.
 *         new MetaSet("年龄", "age"),
 *         new MetaSet("住址", "addr"),
 *         new MetaSet("班主任姓名", "teacher.name"),  //子成员变量
 *         new MetaSet("班主任任教学科", "teacher.xueke.name"), //任意多层引用
 *         new MetaSet("是否欠费", "qianFei"), //boolean字段导出为：是|否
 *         new MetaSet("注册时间", "dtAdd"), //LocalDate自动格式化为：yyyy-MM-dd, LocalDateTime自动格式化为：yyyy-MM-dd HH:mm:ss
 *         new MetaSet("性别", new Formatter<User>() {
 *             public Object fmt(User user) {
 *                 return user.getGender()==1 ? "男":"女";
 *             }
 *         }), //自定义的格式化输出（常规写法）
 *         new MetaSet("性别2", (Formatter&lt;User&gt;) o -> gender==1?"男":"女" )   //自定义的格式化输出(lambla表达式写法)
 *     });
 *
 *     et.addData(pager.getItems()); //添加要导出的数据，可以多次调用. （若数据量大，建议采用使用分页查询，循环导出全部页码的数据，不要一次性查出所有数据，以免因数据太多导致内存溢出）。
 *
 *     et.output(request, response, "查询结果导出"); //导出下载，并指定显示的文件名(.csv扩展名可以省略)
 *     et.output(new File("d:/test.csv")); //导出到文件
 * </pre>
 * <p>
 *
 * @author ziquee(子秋) QQ号:39886616 主页: http://www.jdiy.club
 * @version 20201016
 */
@SuppressWarnings("ALL")
public class CsvUtils implements Export {
    private int index = 0;//行号。
    private boolean isWriteMeta = false;//记录Meta元数据是否已经写入sheet.
    private MetaSet[] meta;
    private List<String[]> datas;

    public CsvUtils() {
    }

    /**
     * 设置表头信息.
     *
     * @param meta sheet 列头定义设置.
     */
    public CsvUtils setHeader(MetaSet[] meta) {
        this.meta = meta;
        return this;
    }

    /**
     * 添加要导出的数据.
     * 调用此方法之前，必须先调用{@link #setHeader(MetaSet[])}方法，完成MetSet表头的设置.
     * 此方法可以多次调用.（若数据量大，建议采用使用分页查询，循环导出全部页码内的数据，不要一次性查出所有数据，以免因数据太多导致内存溢出）。
     *
     * @param dataset 数据集合
     */
    public void addData(Collection dataset) {
        if (meta == null || meta.length < 1) throw new RuntimeException("请先设置表头信息.");
        writeMetaSet();
        if (dataset == null) return;

        //遍历集合数据，产生数据行
        for (Object o : dataset) {
            String[] row = new String[meta.length];
            Object t = o;
            int j = 0;
            for (MetaSet ms : meta) {
                @SuppressWarnings("unchecked")
                Object value = ms.getFormatter().fmt(t);
                if (value == null) row[j] = "";
                else if (value instanceof Boolean) {
                    row[j] = "\"" + ((Boolean) value ? "是" : "否") + "\"";
                } else if (value instanceof LocalDate) {
                    row[j] = "\"" + DateUtils.fmt_date.format((LocalDate) value) + "\"";
                } else if (value instanceof LocalDateTime) {
                    row[j] = "\"" + DateUtils.fmt_datetime.format((LocalDateTime) value) + "\"";
                } else if (value instanceof Date) {
                    Date date = (Date) value;
                    SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
                    row[j] = "\"" + sdf.format(date) + "\"";
                } else if (value instanceof Number) {
                    Number v = (Number) value;
                    if (v.doubleValue() == v.intValue()) row[j] = "" + (v.intValue());
                    else row[j] = "" + v.doubleValue();
                } else {
                    //其它数据类型都当作字符串简单处理
                    String textValue = value.toString();
                    row[j] = "\"" + paraString(textValue) + "\"";
                }
                j++;
            }
            datas.add(row);
        }
    }

    /**
     * Response 根据给定的名称导出并下载
     *
     * @param requeset HttpServletRequest对象
     * @param response HttpServletResponse对象
     * @param fileName 在客户端要显示的名称(可以省略扩展名)
     * @throws IOException
     */
    public void output(HttpServletRequest request, HttpServletResponse response, String fileName) throws IOException {
        writeMetaSet();
        if (fileName == null) fileName = "导出结果.csv";
        else if (!fileName.endsWith(".csv")) fileName += ".csv";
        response.setContentType(MediaType.APPLICATION_OCTET_STREAM.toString());
        response.setHeader("Content-Disposition", "attachment; filename=\"" + getFileName(request, fileName) + "\";");
        output(response.getOutputStream());
    }

    /**
     * 导出到文件
     *
     * @param file 要导出到的文件
     * @throws IOException ex
     */
    public void output(File file) throws IOException {
        writeMetaSet();
        output(new FileOutputStream(file));
    }

    /**
     * 导出到指定的流
     *
     * @param out OutputStream对象
     * @throws IOException ex
     */
    public void output(OutputStream out) throws IOException {
        writeMetaSet();
        try (BufferedWriter buffCvsWriter = new BufferedWriter(new OutputStreamWriter(out, StandardCharsets.UTF_8))){
            buffCvsWriter.write('\ufeff');//写入bom, 防windows excel打开乱码
            // 将body数据写入表格
            boolean f, b = true;
            for (String[] sa : datas) {
                if (!b) buffCvsWriter.newLine();
                f = true;
                for (String s : sa) {
                    if (!f) buffCvsWriter.write(",");
                    buffCvsWriter.write(s);
                    f = false;
                }
                b = false;
            }
            buffCvsWriter.flush();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    private String getFileName(HttpServletRequest request, String name) throws UnsupportedEncodingException {
        String userAgent = request.getHeader("USER-AGENT");
        return userAgent.contains("Mozilla")
                ? new String(name.getBytes(), "ISO8859-1")
                : new String(name.getBytes("gb2312"), "iso8859-1");
    }


    private String paraString(String s) {
        return s.replaceAll("\"", "\"\"");
    }

    private void writeMetaSet() {
        if (isWriteMeta) return;
        isWriteMeta = true;
        datas = new ArrayList<>();
        datas.add(Arrays.stream(meta).map(metaSet -> "\"" + paraString(metaSet.getHead()) + "\"").toArray(String[]::new));
    }
}
